// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AnalyticsClientBase.cs" company="">
//   
// </copyright>
// <summary>
//   The analytics client.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace XRay.Library
{
    #region

    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web;

    using AutoMapper;

    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Analytics;
    using Microsoft.WindowsAzure.Storage.Blob;
    using Microsoft.WindowsAzure.Storage.RetryPolicies;
    using Microsoft.WindowsAzure.Storage.Shared.Protocol;
    using Microsoft.WindowsAzure.Storage.Table;

    using XRay.Library.Contracts;
    using XRay.Library.Helpers;

    #endregion

    /// <summary>
    ///     The analytics client.
    /// </summary>
    public abstract class AnalyticsClientBase : IAnalyticsClient
    {
        #region Fields

        /// <summary>
        /// The azure storage retry policy.
        /// </summary>
        protected readonly IRetryPolicy azureStorageRetryPolicy;

        /// <summary>
        /// The cloud storage account.
        /// </summary>
        protected readonly CloudStorageAccount cloudStorageAccount;

        /// <summary>
        /// The storage location.
        /// </summary>
        private readonly StorageLocation storageLocation;

        /// <summary>
        ///     The service name.
        /// </summary>
        private readonly StorageService storageService;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="AnalyticsClientBase"/> class.
        /// </summary>
        /// <param name="cloudStorageAccount">
        /// The cloud storage account.
        /// </param>
        /// <param name="service">
        /// The service.
        /// </param>
        /// <param name="location">
        /// The location.
        /// </param>
        protected AnalyticsClientBase(
            CloudStorageAccount cloudStorageAccount, 
            StorageService service, 
            StorageLocation location)
        {
            Guard.ArgumentNotNull(cloudStorageAccount, "cloudStorageAccount");
            this.cloudStorageAccount = cloudStorageAccount;
            this.storageService = service;
            this.storageLocation = location;
            this.azureStorageRetryPolicy = new ExponentialRetry(TimeSpan.FromSeconds(5), 30);

            Mapper.CreateMap<MetricsEntity, MetricTransaction>()
                .ForMember(dest => dest.Time, expression => expression.ResolveUsing<PartitionKeyResolver>())
                .ForMember(dest => dest.AccessType, expression => expression.ResolveUsing<AccessTypeResolver>())
                .ForMember(
                    dest => dest.TransactionType, 
                    expression => expression.ResolveUsing<TransactionTypeResolver>());
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// The configure analytics.
        /// </summary>
        /// <param name="loggingProperties">
        /// The logging properties.
        /// </param>
        /// <param name="hourMetrics">
        /// The hour metrics.
        /// </param>
        /// <param name="minuteMetrics">
        /// The minute metrics.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public async Task ConfigureAnalytics(
            LoggingProperties loggingProperties, 
            MetricsProperties hourMetrics, 
            MetricsProperties minuteMetrics, 
            CancellationToken cancellationToken)
        {
            var properties = await this.GetServicePropertiesAsync(cancellationToken);
            properties.Logging = loggingProperties;
            properties.HourMetrics = hourMetrics;
            properties.MinuteMetrics = minuteMetrics;
            await this.SetServicePropertiesAsync(properties, cancellationToken);
        }

        /// <summary>
        /// The get all metrics.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public IEnumerable<MetricTransaction> GetAllHourlyMetrics(DateTimeOffset start, DateTimeOffset end)
        {
            return this.GetAllHourlyMetricsAsync(start, end, CancellationToken.None).Result;
        }

        /// <summary>
        /// The get all metrics async.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public async Task<IEnumerable<MetricTransaction>> GetAllHourlyMetricsAsync(
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken)
        {
            var table = this.cloudStorageAccount.CreateCloudAnalyticsClient()
                .GetHourMetricsTable(this.storageService, this.storageLocation);
            var query = new TableQuery<MetricsEntity>()
                    .Where(GeneratePartitionKeyCondition(start, end));
            return await this.QueryMetricsTable(table, query, cancellationToken);
        }

        /// <summary>
        /// The get all minute metrics.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public IEnumerable<MetricTransaction> GetAllMinuteMetrics(DateTimeOffset start, DateTimeOffset end)
        {
            return this.GetAllMinuteMetricsAsync(start, end, CancellationToken.None).Result;
        }

        /// <summary>
        /// The get all minute metrics async.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public async Task<IEnumerable<MetricTransaction>> GetAllMinuteMetricsAsync(
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken)
        {
            var table = this.cloudStorageAccount.CreateCloudAnalyticsClient()
                .GetMinuteMetricsTable(this.storageService, this.storageLocation);
            var query = new TableQuery<MetricsEntity>()
                    .Where(GeneratePartitionKeyCondition(start, end));
            return await this.QueryMetricsTable(table, query, cancellationToken);
        }

        /// <summary>
        /// The get capacity.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public abstract IEnumerable<BlobCapacity> GetCapacity(DateTimeOffset start, DateTimeOffset end);

        /// <summary>
        /// The get capacity async.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public abstract Task<IEnumerable<BlobCapacity>> GetCapacityAsync(
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken);

        /// <summary>
        /// The get hourly metrics for executed api.
        /// </summary>
        /// <param name="apiName">
        /// The api name.
        /// </param>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public IEnumerable<MetricTransaction> GetHourlyMetricsForExecutedApi(
            TransactionTypeEnum apiName, 
            DateTimeOffset start, 
            DateTimeOffset end)
        {
            return this.GetHourlyMetricsForExecutedApiAsync(apiName, start, end, CancellationToken.None).Result;
        }

        /// <summary>
        /// The get hourly metrics for executed api async.
        /// </summary>
        /// <param name="apiName">
        /// The api name.
        /// </param>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public async Task<IEnumerable<MetricTransaction>> GetHourlyMetricsForExecutedApiAsync(
            TransactionTypeEnum apiName, 
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken)
        {
            var table =
                this.cloudStorageAccount.CreateCloudAnalyticsClient()
                    .GetHourMetricsTable(this.storageService, this.storageLocation);

            return await this.QueryMetricsTableForApi(table, apiName, start, end, cancellationToken);
        }

        /// <summary>
        /// The get log entities.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// </exception>
        public IEnumerable<LogEntity> GetLogEntities(DateTimeOffset start, DateTimeOffset end)
        {
            return this.GetLogEntitiesAsync(start, end, CancellationToken.None).Result;
        }

        /// <summary>
        /// The get log entities async.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <exception cref="ArgumentOutOfRangeException">
        /// </exception>
        public async Task<IEnumerable<LogEntity>> GetLogEntitiesAsync(
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken)
        {
            if (start >= end)
            {
                throw new ArgumentOutOfRangeException("end", "start cannot be later than end");
            }

            var client = this.cloudStorageAccount.CreateCloudBlobClient();

            var analyticsClient = this.cloudStorageAccount.CreateCloudAnalyticsClient();

            var blobs = analyticsClient.ListLogs(
                this.storageService, 
                start, 
                end, 
                LoggingOperations.All, 
                BlobListingDetails.None, 
                new BlobRequestOptions { RetryPolicy = this.azureStorageRetryPolicy }, 
                null);

            var results = await Task.WhenAll(
                blobs.AsParallel().Select(
                    async b =>
                        {
                            var blob =
                                await
                                client.GetBlobReferenceFromServerAsync(
                                    b.Uri, 
                                    null, 
                                    new BlobRequestOptions { RetryPolicy = this.azureStorageRetryPolicy }, 
                                    null, 
                                    cancellationToken);
                            var blobResult = await GetLogItemsForBlob(blob, cancellationToken);
                            return blobResult;
                        }));

            return !results.Any() ? new List<LogEntity>() : results.Aggregate((first, second) => first.Concat(second));
        }

        /// <summary>
        /// The get metrics for executed api.
        /// </summary>
        /// <param name="apiName">
        /// The api name.
        /// </param>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="IEnumerable"/>.
        /// </returns>
        public IEnumerable<MetricTransaction> GetMinuteMetricsForExecutedApi(
            TransactionTypeEnum apiName, 
            DateTimeOffset start, 
            DateTimeOffset end)
        {
            return this.GetMinuteMetricsForExecutedApiAsync(apiName, start, end, CancellationToken.None).Result;
        }

        /// <summary>
        /// The get metrics for executed api async.
        /// </summary>
        /// <param name="apiName">
        /// The api name.
        /// </param>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        public async Task<IEnumerable<MetricTransaction>> GetMinuteMetricsForExecutedApiAsync(
            TransactionTypeEnum apiName, 
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken)
        {
            var table =
                this.cloudStorageAccount.CreateCloudAnalyticsClient()
                    .GetMinuteMetricsTable(this.storageService, this.storageLocation);

            return await this.QueryMetricsTableForApi(table, apiName, start, end, cancellationToken);
        }

        #endregion

        #region Methods

        /// <summary>
        /// The assert analytics is on.
        /// </summary>
        /// <param name="cancellationToken">
        /// The cancellation Token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        protected abstract Task<ServiceProperties> GetServicePropertiesAsync(CancellationToken cancellationToken);

        /// <summary>
        /// The query table async.
        /// </summary>
        /// <param name="table"></param>
        /// <param name="tableQuery">
        ///     The table query.
        /// </param>
        /// <param name="cancellationToken">
        ///     The cancellation token.
        /// </param>
        /// <typeparam name="T">
        /// </typeparam>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        protected async Task<List<T>> QueryTableAsync<T>(CloudTable table, TableQuery<T> tableQuery, CancellationToken cancellationToken)
            where T : ITableEntity, new()
        {
            var results = new List<T>();

            TableContinuationToken tableContinuationToken = null;
            do
            {
                var queryResults =
                    await
                    table.ExecuteQuerySegmentedAsync(
                        tableQuery,
                        tableContinuationToken, 
                        new TableRequestOptions { RetryPolicy = this.azureStorageRetryPolicy }, 
                        null, 
                        cancellationToken);
                tableContinuationToken = queryResults.ContinuationToken;
                results.AddRange(queryResults.Results);
            }
            while (tableContinuationToken != null);
            return results;
        }

        /// <summary>
        /// The set service properties async.
        /// </summary>
        /// <param name="properties">
        /// The properties.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        protected abstract Task SetServicePropertiesAsync(
            ServiceProperties properties, 
            CancellationToken cancellationToken);

        /// <summary>
        /// The generate partition key condition.
        /// </summary>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        private static string GeneratePartitionKeyCondition(DateTimeOffset start, DateTimeOffset end)
        {
            return
                TableQuery.CombineFilters(
                    TableQuery.GenerateFilterCondition(
                        "PartitionKey", 
                        QueryComparisons.GreaterThanOrEqual, 
                        start.ToString("yyyyMMddT0000")), 
                    TableOperators.And, 
                    TableQuery.GenerateFilterCondition(
                        "PartitionKey", 
                        QueryComparisons.LessThanOrEqual, 
                        end.ToString("yyyyMMddT0000")));
        }

        /// <summary>
        /// The get log items for blob.
        /// </summary>
        /// <param name="logBlob">
        /// The log blob.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        private static async Task<IEnumerable<LogEntity>> GetLogItemsForBlob(
            ICloudBlob logBlob, 
            CancellationToken cancellationToken)
        {
            var logLines = await GetLogLinesForBlob(logBlob, cancellationToken);

            return logLines.Select(line => HttpUtility.HtmlDecode(line).Split(';')).Select(
                row =>
                    {
                        DateTimeOffset date;

                        int intVal;
                        long longVal;

                        return new LogEntity
                                   {
                                       VersionNumber = row[0], 
                                       RequestStartTime =
                                           DateTimeOffset.TryParse(row[1], out date)
                                               ? date
                                               : DateTimeOffset.UtcNow, 
                                       OperationType =
                                           (TransactionTypeEnum)
                                           Enum.Parse(typeof(TransactionTypeEnum), row[2], true), 
                                       RequestStatus =
                                           (StatusMessagesEnum)
                                           Enum.Parse(typeof(StatusMessagesEnum), row[3], true), 
                                       HttpStatusCode =
                                           (HttpStatusCode)Enum.Parse(typeof(HttpStatusCode), row[4], true), 
                                       EndtoEndLatencyInMS = int.TryParse(row[5], out intVal) ? intVal : 0, 
                                       ServerLatencyInMS = int.TryParse(row[6], out intVal) ? intVal : 0, 
                                       AuthenticationType = row[7], 
                                       RequestorAccountName = row[8], 
                                       OwnerAccountName = row[9], 
                                       ServiceType =
                                           (AzureStorageServiceEnum)
                                           Enum.Parse(typeof(AzureStorageServiceEnum), row[10], true), 
                                       RequestURL = row[11].Replace("\"", string.Empty), 
                                       RequestedObjectKey = row[12], 
                                       RequestidHeader = new Guid(row[13]), 
                                       OperationCount = int.TryParse(row[14], out intVal) ? intVal : 0, 
                                       RequestorIPAddress = row[15], 
                                       RequestVersionHeader = row[16], 
                                       RequestHeaderSize = long.TryParse(row[17], out longVal) ? longVal : 0, 
                                       RequestPacketSize = long.TryParse(row[18], out longVal) ? longVal : 0, 
                                       ResponseHeaderSize = long.TryParse(row[19], out longVal) ? longVal : 0, 
                                       ResponsePacketSize = long.TryParse(row[20], out longVal) ? longVal : 0, 
                                       RequestContentLength = long.TryParse(row[21], out longVal) ? longVal : 0, 
                                       RequestMD5 = row[22], 
                                       ServerMD5 = row[23], 
                                       EtagIdentifier = row[24], 
                                       LastModifiedTime =
                                           DateTimeOffset.TryParse(row[25], out date)
                                               ? date
                                               : DateTimeOffset.UtcNow, 
                                       ConditionsUsed = row[26], 
                                       UserAgentHeader = row[27], 
                                       ReferrerHeader = row[28], 
                                       ClientRequestId = row[29]
                                   };
                    });
        }

        /// <summary>
        /// The get log lines for blob.
        /// </summary>
        /// <param name="blob">
        /// The blob.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        private static async Task<IEnumerable<string>> GetLogLinesForBlob(
            ICloudBlob blob, 
            CancellationToken cancellationToken)
        {
            var stream = await blob.OpenReadAsync(cancellationToken);
            using (var reader = new StreamReader(stream))
            {
                return
                    (await reader.ReadToEndAsync()).Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
                        .ToList();
            }
        }

        /// <summary>
        /// The query metrics table.
        /// </summary>
        /// <param name="table"></param>
        /// <param name="query">
        ///     The query.
        /// </param>
        /// <param name="cancellationToken">
        ///     The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        private async Task<IEnumerable<MetricTransaction>> QueryMetricsTable(CloudTable table, TableQuery<MetricsEntity> query, CancellationToken cancellationToken)
        {
            var results = await this.QueryTableAsync(table, query, cancellationToken);

            return results.Select(Mapper.Map<MetricsEntity, MetricTransaction>);
        }

        /// <summary>
        /// The query metrics table for api.
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        /// <param name="apiName">
        /// The api name.
        /// </param>
        /// <param name="start">
        /// The start.
        /// </param>
        /// <param name="end">
        /// The end.
        /// </param>
        /// <param name="cancellationToken">
        /// The cancellation token.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        private async Task<IEnumerable<MetricTransaction>> QueryMetricsTableForApi(
            CloudTable table, 
            TransactionTypeEnum apiName, 
            DateTimeOffset start, 
            DateTimeOffset end, 
            CancellationToken cancellationToken)
        {
            var userRowkey = string.Format(
                CultureInfo.InvariantCulture, 
                "{0};{1}", 
                AccessTypeEnum.user, 
                Enum.GetName(typeof(TransactionTypeEnum), apiName));
            var systemRowkey = string.Format(
                CultureInfo.InvariantCulture, 
                "{0};{1}", 
                AccessTypeEnum.system, 
                Enum.GetName(typeof(TransactionTypeEnum), apiName));
            var tableQuery =
                new TableQuery<MetricsEntity>().Where(
                    TableQuery.CombineFilters(
                        TableQuery.CombineFilters(
                            TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, userRowkey), 
                            TableOperators.Or, 
                            TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.Equal, systemRowkey)), 
                        TableOperators.And, 
                        GeneratePartitionKeyCondition(start, end)));


            var results = await this.QueryTableAsync(table, tableQuery, cancellationToken);

            return results.Select(Mapper.Map<MetricsEntity, MetricTransaction>);
        }

        #endregion
    }
}